/*
Xern Entity Relocation Network
Copyright (C) 2011 Ronie Salgado <roniesalg@gmail.com>

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/

#include <stdio.h>
#include <assert.h>
#include "Gpu.hpp"

namespace XernRay
{
    // Texture buffer.
    TextureBuffer::TextureBuffer(int width, int height, int bpp, GLenum internalFormat, GLenum format, GLenum type)
        : width(width), height(height), bpp(bpp), internalFormat(internalFormat), format(format), type(type)
    {
        glGenTextures(1, &textureObject);
        glBindTexture(GL_TEXTURE_2D, textureObject);
        glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, format, type, NULL);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    }

    TextureBuffer::~TextureBuffer()
    {
        glDeleteTextures(1, &textureObject);
    }

    size_t TextureBuffer::GetSize() const
    {
        return width*height*(bpp/8);
    }

    void TextureBuffer::Write(const void *src, size_t offset, size_t amount)
    {
        assert(src);
        assert(offset == 0);
        assert(amount == GetSize());
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, textureObject);
        //glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, format, type, src);
        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, format, type, src);
    }

    void TextureBuffer::Read(void *dst, size_t offset, size_t amount)
    {
        assert(0); // Unsupported.
    }

    void TextureBuffer::Activate(int unit)
    {
        glActiveTexture(GL_TEXTURE0 + unit);
        glBindTexture(GL_TEXTURE_2D, textureObject);
    }

    // Linear buffer.
    LinearBuffer::LinearBuffer(size_t len, GLenum format)
        : size(len)
    {
        // Create the buffer object.
        glGenBuffers(1, &bufferObject);
        glBindBuffer(GL_TEXTURE_BUFFER_EXT, bufferObject);

        // Initialize the buffer object.
        glBufferData(GL_TEXTURE_BUFFER_EXT, size, NULL, GL_DYNAMIC_DRAW);

        // Create the texture.
        glGenTextures(1, &textureObject);
        glBindTexture(GL_TEXTURE_BUFFER_EXT, textureObject);
        glTexBufferEXT(GL_TEXTURE_BUFFER_EXT, format, bufferObject);
        glBindBuffer(GL_TEXTURE_BUFFER_EXT, 0);
    }

    LinearBuffer::~LinearBuffer()
    {
        glDeleteTextures(1, &textureObject);
        glDeleteBuffers(1, &bufferObject);
    }

    size_t LinearBuffer::GetSize() const
    {
        return size;
    }

    void LinearBuffer::Write(const void *src, size_t offset, size_t amount)
    {
        glBindBuffer(GL_TEXTURE_BUFFER_EXT, bufferObject);
        glBufferSubData(GL_TEXTURE_BUFFER_EXT, offset, amount, src);
        glBindBuffer(GL_TEXTURE_BUFFER_EXT, 0);
    }

    void LinearBuffer::Read(void *dst, size_t offset, size_t amount)
    {
        glBindBuffer(GL_TEXTURE_BUFFER_EXT, bufferObject);
        glGetBufferSubData(GL_TEXTURE_BUFFER_EXT, offset, amount, dst);
        glBindBuffer(GL_TEXTURE_BUFFER_EXT, 0);
    }

    void LinearBuffer::Activate(int unit)
    {
        glActiveTexture(GL_TEXTURE0 + unit);
        glBindTexture(GL_TEXTURE_BUFFER_EXT, textureObject);
    }

    // Target buffer.
    TargetBuffer::TargetBuffer(int width, int height)
    {
        glGenFramebuffersEXT(1, &fbo);
    }

    TargetBuffer::~TargetBuffer()
    {
        glDeleteFramebuffersEXT(1, &fbo);
    }

    void TargetBuffer::Activate()
    {
        glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo);
    }

    void TargetBuffer::AttachBuffer(int id, boost::shared_ptr<TextureBuffer> buffer)
    {
        Activate();
        GLuint texId = 0;
        if(buffer)
            texId = buffer->textureObject;

        glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo);
        glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT + id, GL_TEXTURE_2D, texId, 0);
    }

    // Ping-pong buffer.
    PingPongBuffer::PingPongBuffer(boost::shared_ptr<TargetBuffer> target, int id,
                                    boost::shared_ptr<TextureBuffer> front, boost::shared_ptr<TextureBuffer> back)
        : target(target), targetId(id), front(front), back(back)
    {
        assert(front->GetWidth() == back->GetWidth());
        assert(front->GetHeight() == back->GetHeight());
        assert(front->GetSize() == back->GetSize());
        activeUnit = 0;
        target->AttachBuffer(targetId, back);
    }

    PingPongBuffer::~PingPongBuffer()
    {
    }

    size_t PingPongBuffer::GetSize() const
    {
        return front->GetSize();
    }

    void PingPongBuffer::Write(const void *src, size_t offset, size_t amount)
    {
        front->Write(src, offset, amount);
    }

    void PingPongBuffer::Read(void *dst, size_t offset, size_t amount)
    {
        front->Read(dst, offset, amount);
    }

    void PingPongBuffer::Activate(int unit)
    {
        activeUnit = unit;
        front->Activate(unit);
    }

    void PingPongBuffer::Pong()
    {
        boost::shared_ptr<TextureBuffer> temp = front;
        front = back;
        back = temp;
        front->Activate(activeUnit);
        target->AttachBuffer(targetId, back);
    }

    // Program.
    Program::Program()
    {
    }

    Program::~Program()
    {
        glDetachShader(program, vertex);
        glDetachShader(program, fragment);

        glDeleteProgram(program);
        glDeleteShader(vertex);
        glDeleteShader(fragment);
    }

    void Program::Activate()
    {
        glUseProgram(program);
    }

    int Program::GetUniformLocation(const std::string &name) const
    {
        int ret = glGetUniformLocation(program, name.c_str());
        return ret;
    }

    void Program::SetUniform(int loc, int v1)
    {
        glUniform1i(loc, v1);
    }

    static GLuint CreateShader(GLenum type, const std::string &src, const std::string &name = "")
    {
        // Create the shader.
        GLuint ret = glCreateShader(type);

        // Set the shader source.
        GLint len = src.size();
        const GLchar *source = src.c_str();
        glShaderSource(ret, 1, &source, &len);

        // Compile the shader.
        glCompileShader(ret);

        // Check for erros.
        GLint status;
        glGetShaderiv(ret, GL_COMPILE_STATUS, &status);
        if(status != GL_TRUE)
        {
            // Print the errors.
            GLint infoLen = 1;
            glGetShaderiv(ret, GL_INFO_LOG_LENGTH, &infoLen);

            char *buffer = new char[infoLen+1];
            glGetShaderInfoLog(ret, infoLen, NULL, buffer);
            buffer[infoLen] = 0;
            fprintf(stderr, "Failed to compile shader '%s': %s\n", name.c_str(), buffer);
            delete buffer;
            exit(1);
        }

        return ret;
    }

    boost::shared_ptr<Program> Program::CreateFromCode(const std::string &vertex, const std::string &fragment, const std::string &name)
    {
        // Create the shaders.
        GLuint vertexShader = CreateShader(GL_VERTEX_SHADER, vertex, name);
        GLuint fragmentShader = CreateShader(GL_FRAGMENT_SHADER, fragment, name);

        // Create the program.
        GLuint program = glCreateProgram();

        // Attach the shaders.
        glAttachShader(program, vertexShader);
        glAttachShader(program, fragmentShader);

        // Link the program.
        glLinkProgram(program);

        // Check for errors.
        GLint status;
        glGetProgramiv(program, GL_LINK_STATUS, &status);
        if(status != GL_TRUE)
        {
            // Print the errors.
            GLint infoLen = 1;
            glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLen);

            char *buffer = new char[infoLen+1];
            glGetProgramInfoLog(program, infoLen, NULL, buffer);
            buffer[infoLen] = 0;
            fprintf(stderr, "Failed to link program '%s': %s\n", name.c_str(), buffer);
            delete buffer;
            exit(1);
        }
        
        // Create the return object.
        boost::shared_ptr<Program> ret(new Program());
        ret->vertex = vertexShader;
        ret->fragment = fragmentShader;
        ret->program = program;
        return ret;
    }

    boost::shared_ptr<Program> Program::CreateFromFile(const std::string &vertex, const std::string &fragment)
    {
        FILE *vertexFile = fopen(vertex.c_str(), "rb");
        if(!vertexFile)
        {
            fprintf(stderr, "Failed to open vertex shader source file '%s'\n", vertex.c_str());
            exit(1);
        }

        FILE *fragmentFile = fopen(fragment.c_str(), "rb");
        if(!fragmentFile)
        {
            fclose(vertexFile);
            fprintf(stderr, "Failed to open vertex shader source file '%s'\n", fragment.c_str());
            exit(1);
        }

        fseek(vertexFile, 0, SEEK_END);
        size_t vertexLen = ftell(vertexFile);
        fseek(vertexFile, 0, SEEK_SET);

        fseek(fragmentFile, 0, SEEK_END);
        size_t fragmentLen = ftell(fragmentFile);
        fseek(fragmentFile, 0, SEEK_SET);

        char *vertexBuffer = new char[vertexLen+1];
        char *fragmentBuffer = new char[fragmentLen+1];

        fread(vertexBuffer, vertexLen, 1, vertexFile);
        fread(fragmentBuffer, fragmentLen, 1, fragmentFile);

        vertexBuffer[vertexLen] = 0;
        fragmentBuffer[fragmentLen] = 0;

        fclose(vertexFile);
        fclose(fragmentFile);

        boost::shared_ptr<Program> ret = CreateFromCode(vertexBuffer, fragmentBuffer, /* name */ vertex);

        delete [] vertexBuffer;
        delete [] fragmentBuffer;

        return ret;
    }

};
